package com.katesoft.scale4j.common.spring;

import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.log.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.core.Ordered;
import org.springframework.util.ClassUtils;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

/**
 * abstract class for post processing, contains logic for setting bean properties to provided. should be used for internal purpose or exposed with wrapped
 * class.
 * <p/>
 * This class itself does not implement {@link org.springframework.core.Ordered} or {@link org.springframework.core.PriorityOrdered},
 * but instead define order
 * property and provide getter/setter for this order property. So actual subclasses may just implement Ordered or PriorityOrdered interface and set order.
 *
 * @author katesoft2007
 */
public abstract class AbstractBeanPropertiesOverridePostProcessor implements BeanFactoryPostProcessor
{
    protected transient Logger logger = LogFactory.getLogger(getClass());
    private String targetBean;
    private transient ConfigurableListableBeanFactory configurableListableBeanFactory;
    private boolean enabled = true;
    private Integer order = Ordered.LOWEST_PRECEDENCE / 2;
    //
    protected Map<Object, Object> beanProperties;
    protected transient MutablePropertyValues mutablePropertyValues;
    protected transient ConstructorArgumentValues constructorArgumentValues;

    @Override
    public void postProcessBeanFactory(final ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException
    {
        this.configurableListableBeanFactory = configurableListableBeanFactory;
        if (enabled) {
            mutablePropertyValues = getBeanDefinition().getPropertyValues();
            constructorArgumentValues = getBeanDefinition().getConstructorArgumentValues();
            doPostProcessing(configurableListableBeanFactory);
        }
    }

    /** overrides bean properties with those, that provided by client. */
    protected void overrideBeanProperties()
    {
        if (beanProperties != null) {
            for (Entry<Object, Object> next : beanProperties.entrySet()) {
                Object beanPropertyValue = next.getValue();
                String beanPropertyName = (String) next.getKey();
                overridePropertyDefinition(beanPropertyName, beanPropertyValue);
            }
        }
    }

    protected void overridePropertyDefinition(String beanPropertyName,
                                              Object beanPropertyValue)
    {
        PropertyValue propertyValue = mutablePropertyValues.getPropertyValue(beanPropertyName);
        Class<?> propertyType = BeanUtils.findPropertyType(beanPropertyName, new Class[]{getActualBeanClass()});
        logger.debug("writing %s[%s]=(%s)", beanPropertyName, propertyType.getSimpleName(), beanPropertyValue);
        if (propertyValue == null) {
            mutablePropertyValues.addPropertyValue(beanPropertyName, beanPropertyValue);
        }
        else {
            mutablePropertyValues.removePropertyValue(beanPropertyName);
            propertyValue = new PropertyValue(beanPropertyName, beanPropertyValue);
            mutablePropertyValues.addPropertyValue(propertyValue);
        }
    }

    /** @return actual class of bean definition. */
    protected Class<?> getActualBeanClass()
    {
        try {
            return ClassUtils.forName(configurableListableBeanFactory.getBeanDefinition(targetBean).getBeanClassName(), ClassUtils.getDefaultClassLoader());
        }
        catch (final ClassNotFoundException e) {
            throw new FatalBeanException("Unable to load class meta-information", e);
        }
    }

    /**
     * attempt to covert given object to map
     *
     * @param value map/properties/string
     * @return properties
     */
    protected Properties convertToProperties(Object value)
    {
        if (value != null) {
            SimpleTypeConverter converter = new SimpleTypeConverter();
            return converter.convertIfNecessary(value, Properties.class);
        }
        else { return new Properties(); }
    }

    /** @return bean definition of targetBean. */
    protected BeanDefinition getBeanDefinition()
    {
        if (configurableListableBeanFactory.containsBeanDefinition(targetBean)) {
            return configurableListableBeanFactory.getBeanDefinition(targetBean);
        }
        throw new NoSuchBeanDefinitionException(
            String.format("No bean named [%s] exists within the bean factory. Cannot post process bean definitions!", targetBean));
    }

    /**
     * client bean properties override
     *
     * @param beanProperties properties to override(apply)
     */
    public void setBeanProperties(Map<Object, Object> beanProperties)
    {
        this.beanProperties = beanProperties;
    }

    /**
     * allows to disable this post processor.
     *
     * @param enabled true or false flag
     */
    public void setEnabled(boolean enabled)
    {
        this.enabled = enabled;
    }

    public int getOrder()
    {
        return order == null ? 0 : order;
    }

    /**
     * The obvious order the post processors execute would of course be the order in which they’re defined in the XML file, one of the others said.
     * <p/>
     * Unfortunately, you can’t rely on the order of the beans defined in a Spring XML file.
     * <p/>
     * The order in which they’re defined is not guaranteed to be the order in which they’re executed.
     * <p/>
     * This method allows to set the order of bean factory post processor.
     *
     * @param order preferred execution order of this post processor.
     */
    public void setOrder(Integer order)
    {
        this.order = order;
    }

    /**
     * set the bean name. This property is not required in this abstract class.
     *
     * @param targetBean name/id of spring bean.
     */
    protected void setTargetBean(String targetBean)
    {
        this.targetBean = targetBean;
    }

    /** @return name of the target bean for properties overriding. */
    public String getTargetBean()
    {
        return targetBean;
    }

    /**
     * perform actual post-processing.
     *
     * @param configurableListableBeanFactory
     *         bean factory
     */
    protected abstract void doPostProcessing(ConfigurableListableBeanFactory configurableListableBeanFactory);
}
